package com.nulldev.util.web.YouTubeExtractor.channel;

import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.CLIENT_VERSION;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_CL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_LABEL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_STS;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.UTC_OFFSET;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.VARIANTS_CHECKSUM;
import static com.nulldev.util.web.YouTubeExtractor.channel.ChannelUtil.__ENSURE_ARRAY;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getTabRendererWithContent;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getYTString;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.internal.backport.concurrency9.Lists;
import com.nulldev.util.internal.backport.concurrency9.Maps;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpMethod;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.util.YTRequestObject;

public class ChannelCommunityExtractor {

	private static final Logger log = LoggerFactory.getLogger();

	public static class CommunityImage {
		private final int width, height;
		private final String url;
		private final int idx;

		public CommunityImage(final int width, final int height, final String url) {
			this(width, height, url, 0);
		}

		public CommunityImage(final int width, final int height, final String url, final int idx) {
			this.width = width;
			this.height = height;
			this.url = url;
			this.idx = idx;
		}

		public int getWidth() {
			return this.width;
		}

		public int getHeight() {
			return this.height;
		}

		public String getURL() {
			return this.url;
		}

		public int getIndex() {
			return this.idx;
		}

		@Override
		public String toString() {
			return "CommunityImage[width=" + this.width + ",height=" + this.height + ",url=" + this.url + "]";
		}
	}

	public static class CommunityVideo {
		private final String videoID;

		public CommunityVideo(final String videoID) {
			this.videoID = videoID;
		}

		public String getVideoID() {
			return this.videoID;
		}

		@Override
		public String toString() {
			return "CommunityVideo[videoID=" + this.videoID + "]";
		}
	}

	public static class CommunityPoll {
		/**
		 * Poll: Are you alive? <br>
		 * · No <br>
		 * · No
		 */
		private final List<String> pollOptions = FastLists.list();

		public CommunityPoll(final String... options) {
			this.pollOptions.addAll(Lists.of(options));
		}

		public CommunityPoll(final List<String> options) {
			this.pollOptions.addAll(options);
		}

		public List<String> getOptions() {
			return this.pollOptions;
		}
	}

	public static class CommunityEntry {
		private String author;
		private String comment;
		private final List<List<CommunityImage>> images;
		private final List<CommunityVideo> videos = FastLists.list();
		private final List<CommunityPoll> polls = FastLists.list();

		public CommunityEntry(final String author, final String comment) {
			this.images = FastLists.list();
			this.author = author;
			this.comment = comment;
		}

		public String getAuthor() {
			return this.author;
		}

		public String getComment() {
			return this.comment;
		}

		public List<List<CommunityImage>> getImages() {
			return this.images;
		}

		protected CommunityEntry addImage(final List<CommunityImage> imgs) {
			this.images.add(imgs);
			return this;
		}

		public boolean hasImages() {
			return this.images != null && !this.images.isEmpty();
		}

		public boolean hasVideos() {
			return this.videos != null && !this.videos.isEmpty();
		}

		public boolean hasPolls() {
			return this.polls != null && !this.polls.isEmpty();
		}

		public List<CommunityPoll> getPolls() {
			return this.polls;
		}

		public List<CommunityVideo> getVideos() {
			return this.videos;
		}

		protected CommunityEntry addVideo(final CommunityVideo vid) {
			this.videos.add(vid);
			return this;
		}

		protected CommunityEntry addPoll(final CommunityPoll vid) {
			this.polls.add(vid);
			return this;
		}

		@Override
		public String toString() {
			return "CommunityEntry[author=" + this.author + ",content=" + this.comment + ",images=" + this.images + ",videos=" + this.videos + "]";
		}
	}

	public static class CommunityChannel {
		private final List<CommunityEntry> entries;
		private String conts;

		public CommunityChannel() {
			this(Lists.of());
		}

		public CommunityChannel(final List<CommunityEntry> entries) {
			this.entries = entries;
		}

		public List<CommunityEntry> getEntries() {
			return this.entries;
		}

		public boolean hasContinuations() {
			return this.conts != null && !this.conts.isEmpty();
		}

		public String getContinuation() {
			return this.conts;
		}

		protected CommunityChannel setContinuation(final String cont) {
			this.conts = cont;
			return this;
		}
	}

	public static CommunityChannel extract(final String channelID) throws Exception {
		return extract(channelID, true);
	}

	public static CommunityChannel extract(final String channelID, final String continuation) throws Exception {
		return extract(channelID, continuation, true);
	}

	public static CommunityChannel extract(final String channelID, final boolean sendConsentCookie) throws Exception {
		return extract(channelID, null, sendConsentCookie);
	}

	public static CommunityChannel extract(String channelID, final String _continuation, final boolean sendConsentCookie) throws Exception {
		final boolean hasCont = _continuation != null && !_continuation.isEmpty() && !_continuation.equalsIgnoreCase("null");
		final String result;
		if (hasCont) {
			final UniJsonValue o = YTRequestObject.makeYTRequestObject(Maps.of());
			o.add("continuation", _continuation);
			result = HttpClient.getString().url("https://www.youtube.com/youtubei/v1/browse?key=" + YouTubeExtractorConstants.API_KEY + "&prettyPrint=false")
					.timeout(Duration.ofSeconds(30)).headers(sendConsentCookie ? YouTubeExtractor.DEFAULT_COOKIES : YouTubeExtractor.BLANK_COOKIES)
					.header("X-YouTube-Client-Name", "1").header("X-YouTube-Client-Version", CLIENT_VERSION).header("X-YouTube-Page-CL", PAGE_CL)
					.header("X-YouTube-Page-Label", PAGE_LABEL).header("X-YouTube-STS", PAGE_STS).header("X-YouTube-Utc-Offset", UTC_OFFSET)
					.header("X-YouTube-Variants-Checksum", VARIANTS_CHECKSUM).header(new Entry("Content-Type", "application/json"))
					.create(HttpMethod.POST, o.toString()).dataThenClose();
		} else {
			String URL = "https://www.youtube.com/channel/" + channelID + "/community?pbj=1";
			if (channelID.startsWith("/user/")) {
				channelID = channelID.substring(1);
				URL = "https://www.youtube.com/" + channelID + "/community?pbj=1";
			} else if (channelID.startsWith("/") && !channelID.startsWith("/user/")) {
				URL = "https://www.youtube.com/user/" + channelID + "community?pbj=1";
			}
			result = HttpClient.getString().url(URL).timeout(Duration.ofSeconds(30))
					.headers(sendConsentCookie ? YouTubeExtractor.DEFAULT_COOKIES : YouTubeExtractor.BLANK_COOKIES).header("X-YouTube-Client-Name", "1")
					.header("X-YouTube-Client-Version", CLIENT_VERSION).header("X-YouTube-Page-CL", PAGE_CL).header("X-YouTube-Page-Label", PAGE_LABEL)
					.header("X-YouTube-STS", PAGE_STS).header("X-YouTube-Utc-Offset", UTC_OFFSET).header("X-YouTube-Variants-Checksum", VARIANTS_CHECKSUM)
					.create().dataThenClose();
		}
		if (result.startsWith("[") || result.endsWith("]")) {
			final UniJsonValue object = UniJsonValue.parseArray(result);
			if (object == null || object.isNull()) {
				log.error("Failed to parse JSON payload!");
				log.debug("Payload: " + result);
				return new CommunityChannel();
			}
			final UniJsonValue pbo = object.arrayGetContains("response");
			final UniJsonIterableValue tabs = pbo.mget("response.contents.twoColumnBrowseResultsRenderer.tabs").asArray();
			UniJsonValue communityTab = null;
			for (final UniJsonValue tab : tabs) {
				if (!tab.contains("tabRenderer")) {
					log.warn("Invalid tab: " + tab);
					continue;
				}
				final UniJsonValue tabRenderer = tab.get("tabRenderer");
				if (tabRenderer.contains("content")) {
					communityTab = tabRenderer;
					break;
				}
			}
			if (communityTab == null)
				throw new NullPointerException("communityTab not found!");
			final UniJsonValue isr = communityTab.get("content").get("sectionListRenderer").get("contents").first().get("itemSectionRenderer");
			final UniJsonIterableValue entries = isr.get("contents").asArray();
			final boolean hasConts = isr.contains("continuations");
			String continuation = null;
			if (hasConts) {
				continuation = isr.get("continuations").first().get("nextContinuationData").getString("continuation");
			}
			final List<CommunityEntry> commEntries = FastLists.list();
			continuation = __process_entries(entries, commEntries);
			return new CommunityChannel(commEntries).setContinuation(continuation);
		} else {
			final UniJsonValue object = UniJsonValue.parse(result);
			if (object == null || object.isNull()) {
				log.error("Failed to parse JSON payload!");
				log.debug("Payload: " + result);
				return new CommunityChannel();
			} else {
				if (!object.contains("onResponseReceivedEndpoints", "response")) {
					log.error("Unknown result type!");
					log.derror(result);
					return new CommunityChannel();
				}
				if (object.contains("response")) {
					final UniJsonValue response = object.get("response");
					if (!response.contains("contents")) {
						log.error("Unknown response type!");
						return new CommunityChannel();
					}
					final UniJsonValue contents = response.get("contents");
					if (!contents.contains("twoColumnBrowseResultsRenderer")) {
						log.error("Unknown response/contents type!");
						return new CommunityChannel();
					}
					final UniJsonValue renderer = contents.get("twoColumnBrowseResultsRenderer");
					final UniJsonIterableValue tabs = renderer.get("tabs").asArray();
					final UniJsonValue playlistContent = getTabRendererWithContent(tabs);
					if (playlistContent == null) {
						log.error("Failed to get playlist from tab renderer!");
						return new CommunityChannel();
					}
					final UniJsonValue entries = playlistContent.get("tabRenderer").get("content").get("sectionListRenderer").get("contents");
					__ENSURE_ARRAY(entries);
					final UniJsonValue first = entries.first();
					if (!first.contains("itemSectionRenderer")) {
						log.error("Failed to get itemSectionRenderer from contents!");
						return new CommunityChannel();
					}
					final UniJsonValue itemSectionRenderer = first.get("itemSectionRenderer");
					final UniJsonIterableValue itemSectionRendererContents = itemSectionRenderer.get("contents").asArray();
					final List<CommunityEntry> commEntries = FastLists.list();
					final String cont = __process_entries(itemSectionRendererContents, commEntries);
					return new CommunityChannel(commEntries).setContinuation(cont);
				} else {
					final UniJsonValue onResponseRecievedEndpoints = object.get("onResponseReceivedEndpoints");
					if (!onResponseRecievedEndpoints.isArray()) {
						log.error("Unknown onResponseRecievedEndpoints type!");
						log.debug(result);
						return new CommunityChannel();
					}
					final UniJsonValue appendContinuationItemsAction = onResponseRecievedEndpoints.first().get("appendContinuationItemsAction");
					final UniJsonIterableValue contItems = appendContinuationItemsAction.get("continuationItems").asArray();
					final List<CommunityEntry> commEntries = FastLists.list();
					final String cont = __process_entries(contItems, commEntries);
					return new CommunityChannel(commEntries).setContinuation(cont);
				}
			}
		}
	}

	private static String __process_entries(final UniJsonIterableValue entries, final List<CommunityEntry> commEntries) {
		String continuation = null;
		for (final UniJsonValue _entry : Variables.requireNonNullObject(entries, "entries")) {
			if (!_entry.contains("backstagePostThreadRenderer", "channelVideoPlayerRenderer", "continuationItemRenderer", "sharedPostRenderer")) {
				log.warn("Unknown object: " + _entry);
				continue;
			} else if (_entry.contains("channelVideoPlayerRenderer")) {
				final UniJsonValue entry = _entry.get("channelVideoPlayerRenderer");
				final CommunityEntry cme = new CommunityEntry("", "");
				cme.addVideo(new CommunityVideo(entry.getString("videoId")));
				commEntries.add(cme);
			} else if (_entry.contains("continuationItemRenderer")) {
				final UniJsonValue entry = _entry.get("continuationItemRenderer");
				final String token = entry.get("continuationEndpoint").get("continuationCommand").getString("token");
				if (continuation == null) {
					continuation = token;
				} else
					log.warn("Ignored unknown continuation token: " + token);
			} else {
				final UniJsonValue entry;
				if (_entry.contains("sharedPostRenderer")) {
					entry = _entry.get("sharedPostRenderer");
				} else {
					entry = _entry.get("backstagePostThreadRenderer");
				}
				if (!entry.contains("post", "originalPost")) {
					log.warn("Entry has no post: " + entry);
					continue;
				}
				UniJsonValue _post = entry.getEither("post", "originalPost");
				if (_post.contains("sharedPostRenderer")) {
					_post = _post.get("sharedPostRenderer").getEither("post", "originalPost");
				}
				final UniJsonValue post = _post;
				if (!post.contains("backstagePostRenderer")) {
					log.warn("Unknown post type: " + post);
				} else {
					final UniJsonValue renderer = post.get("backstagePostRenderer");
					String authorText = getYTString(renderer.get("authorText"));
					if (authorText == null) {
						try {
							authorText = renderer.get("authorText").get("runs").first().getString("text");
						} catch (Exception e) {
							log.error("Failed to get authorText! [method=runs]", e);
						}
					}
					if (authorText == null) {
						log.warn("Failed to get authorText: " + renderer);
					}
					String contentText = null;
					if (renderer.contains("contentText") && renderer.get("contentText").contains("runs")) {
						for (final UniJsonValue line : renderer.get("contentText").getSafe("runs").asArray()) {
							if (contentText == null) {
								if (line.contains("text")) {
									contentText = line.getString("text");
								} else
									contentText = line.getString("line");
							} else {
								if (!line.contains("line", "text")) {
									log.warn("Invalid line: " + line);
									continue;
								} else if (line.contains("text")) {
									contentText += line.getString("text");
								} else {
									contentText += line.getString("line");
								}
							}
						}
					} else
						contentText = "";
					final CommunityEntry cme = new CommunityEntry(authorText, contentText);
					final List<CommunityImage> imgList = FastLists.list();
					if (renderer.hasValue("backstageAttachment")) {
						final UniJsonValue attachment = renderer.get("backstageAttachment");
						if (attachment.contains("backstageImageRenderer")) {
							final UniJsonValue image = attachment.get("backstageImageRenderer");
							final UniJsonIterableValue images = image.get("image").get("thumbnails").asArray();
							images.forEach((img) -> {
								final int width = img.getInt("width");
								final int height = img.getInt("height");
								final String url = img.getString("url");
								imgList.add(new CommunityImage(width, height, url, 0));
							});
						} else if (attachment.contains("videoRenderer")) {
							final UniJsonValue video = attachment.get("videoRenderer");
							final String videoID = video.getString("videoId");
							cme.addVideo(new CommunityVideo(videoID));
						} else if (attachment.contains("pollRenderer")) {
							final UniJsonValue poll = attachment.get("pollRenderer");
							final List<String> choices = new ArrayList<String>(poll.get("choices").size());
							poll.get("choices").asArray().forEach(choice -> {
								choices.add(choice.get("text").get("runs").first().getString("text"));
							});
							cme.addPoll(new CommunityPoll(choices));
						} else if (attachment.contains("postMultiImageRenderer")) {
							final UniJsonValue imageMulti = attachment.get("postMultiImageRenderer");
							final UniJsonIterableValue attachments = imageMulti.get("images").asArray();
							final AtomicInteger idx = new AtomicInteger();
							attachments.forEach((postImg) -> {
								if (!postImg.contains("backstageImageRenderer")) {
									log.warn("Unknown multi-attachment: " + postImg);
									return;
								}
								final UniJsonValue image0 = postImg.get("backstageImageRenderer");
								final UniJsonIterableValue images = image0.get("image").get("thumbnails").asArray();
								final int c_idx = idx.getAndIncrement();
								images.forEach((img) -> {
									final int width = img.getInt("width");
									final int height = img.getInt("height");
									final String url = img.getString("url");
									imgList.add(new CommunityImage(width, height, url, c_idx));
								});
							});
						} else {
							log.warn("Unknown attachment: " + attachment);
						}
					}
					cme.addImage(imgList);
					commEntries.add(cme);
				}
			}
		}
		return continuation;
	}

	public static void main(String[] args) throws Exception {
		final CommunityChannel cc = extract("UCsgnjvCvJZgAdSMMRG_j0cw",
				"4qmFsgKBARIYVUNzZ25qdkN2SlpnQWRTTU1SR19qMGN3GkxFZ2xqYjIxdGRXNXBkSG00QVFDcUF4d0tHRkV3TkhSTmFtaDNWVlZrUmxSWGF6TmFNMWt3VVdjOVBTZ0s4Z1lFQ2dKS0FBJTNEJTNEmgIWYmFja3N0YWdlLWl0ZW0tc2VjdGlvbg%3D%3D");
		JVM.println(cc);
		JVM.println("cont: " + cc.getContinuation());
		JVM.println("entries: " + cc.getEntries());
	}
}
